/* NOTICE
 
    Jim - the Java Image Manager, copyright (c) 2005 - 2007 Grant Gardner 
 
    Jim is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Jim is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
  
    In addition, as a special exception, you are permitted to link this code
    with the 3rd party libraries required to compile this file and to
    distribute a linked combination including them. You must obey the
    GNU General Public License in all respects for all of the code except for
    those 3rd party libraries that are licenced under terms incompatible with the
    GNU General Public License.  
 
 
 NOTICE */

/*
 * Created on 17/06/2004
 *
 */

package au.com.lastweekend.jim.io.exif;

import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.apache.log4j.Logger;

import au.com.lastweekend.jim.io.metadata.Directory;
import au.com.lastweekend.jim.io.metadata.Tag;
import au.com.lastweekend.jim.util.LoggingOutputStream;

/**
 * @author grant@lastweekend.com.au
 * @version $Id: AbstractTiffTagWriter.java,v 1.3 2006/02/20 12:21:10 ggardner Exp $
 * 
 */
public abstract class AbstractTiffTagWriter implements DirectoryWriter {

    private final Directory directory;
    private TiffTagFormat dataFormat;
    private int startOffset;
    private static final Logger LOG = Logger.getLogger(AbstractTiffTagWriter.class);
    private ExifTransform transform;

    protected AbstractTiffTagWriter(Directory directory, ExifTransform exifTransform) {

        this.transform = exifTransform;
        this.directory = directory;
    }

    public void writeHeader(OutputStream output) throws IOException {

        if (dataFormat.isMotorolaByteOrder()) {
            output.write('M');
            output.write('M');
        } else {
            output.write('I');
            output.write('I');
        }

        dataFormat.write16bits(ExifDirectory.EXIF_DIRECTORY_IDENTIFIER, output);
        // some directories may have originally started at a different offset
        // than 8,
        // but we will always write back at 8.
        dataFormat.write32bits(startOffset, output);
    }

    public void writeDirectory(OutputStream output) throws IOException {

        if (LOG.isDebugEnabled()) {
            debugStream("Writing " + getDirectory().getName(), output);
        }
        writeTags(output);
    }

    protected void writeTags(OutputStream output) throws IOException {

        int startOffset = getStartOffset();
        int writeableTagCount = getWriteableTagCount();
        int nextTagValueOffset = startOffset + 2 + (12 * writeableTagCount) + 4;

        debugStream("TagCount: " + writeableTagCount, output);
        dataFormat.write16bits(writeableTagCount, output);

        List<Object> longTagList = new ArrayList<Object>();

        Iterator<Tag> tagIterator = getWriteableTagIterator();
        while (tagIterator.hasNext()) {
            Tag tag = tagIterator.next();
            nextTagValueOffset = writeTagInfo(tag, nextTagValueOffset, longTagList, output);

        }

        // Linked directory comes after all the tag values, Write the offset
        // value now.
        DirectoryWriter linkedDirectoryWriter = getLinkedDirectoryWriter(nextTagValueOffset);
        int linkedDirectoryLength = linkedDirectoryWriter.getDirectoryLength();

        int linkedDirectoryOffset = (linkedDirectoryLength == 0) ? 0 : nextTagValueOffset;
        dataFormat.write32bits(linkedDirectoryOffset, output);

        for (Iterator<Object> iter = longTagList.iterator(); iter.hasNext();) {
            Object longTag = iter.next();
            writeLongTag(longTag, output);
        }

        linkedDirectoryWriter.writeDirectory(output);

    }

    protected void writeLongTag(Object longTag, OutputStream output) throws IOException {

        if (longTag instanceof DirectoryWriter) {
            DirectoryWriter directoryWriter = (DirectoryWriter) longTag;
            directoryWriter.writeDirectory(output);
        } else {

            Tag tag = (Tag) longTag;
            if (LOG.isDebugEnabled()) {
                debugStream("Long tag: " + tag, output);
            }

            int tagType = tag.getTagType();
            Object tagValue = getTransformedTagValue(tagType);
            int formatCode = tag.getFormatCode();

            dataFormat.writeValue(tagValue, formatCode, output);
        }
    }

    /**
     * 
     * @param tag
     *            Tag to write
     * @param nextTagValueOffset
     *            The currently avaliable tag offset
     * @param longTagList
     *            If tag doesn't fit into info space (4 bytes), either it or a DirectoryWriter will be added to this list
     * @param output
     *            OutputStream to write to
     * @return next available tag value offset
     * @throws IOException
     */
    protected int writeTagInfo(Tag tag, final int nextTagValueOffset, List<Object> longTagList, OutputStream output)
            throws IOException {

        int formatCode = tag.getFormatCode();
        int tagType = tag.getTagType();
        Object tagValue = getTransformedTagValue(tagType);

        int componentCount = 0;
        int fillLength = 0;
        boolean writeOffset = false;
        // nextTagValueOffset is final as a reminder that it doesn't get written
        // until AFTER
        // we've worked out what the nextNext offset will be, so don't +=
        // nextTagValueOffset, set returnOffset instead.
        int returnOffset = nextTagValueOffset;

        if (tagValue instanceof Directory) {
            DirectoryWriter directoryWriter = getDirectoryWriter(tagType, (Directory) tagValue, nextTagValueOffset);
            if (directoryWriter instanceof SkipTagDirectoryWriter) {
                tagType = 0xAAAA;
                formatCode = TiffTagFormat.FMT_ULONG;
                componentCount = 1;
                tagValue = 0;
            } else {
                writeOffset = true;
                componentCount = directoryWriter.getComponentCount();
                returnOffset = nextTagValueOffset + directoryWriter.getDirectoryLength();
                longTagList.add(directoryWriter);
            }

        } else {
            componentCount = dataFormat.getComponentCount(tagValue);
            int valueLength = dataFormat.getByteCount(formatCode, componentCount);
            if (valueLength > 4) {
                returnOffset = nextTagValueOffset + getTagDataLength(tagType, tagValue, formatCode);
                longTagList.add(tag);
                writeOffset = true;
            } else {
                fillLength = 4 - valueLength;
            }
        }

        if (writeOffset) {
            writeOffsetTagInfoHeader(tagType, formatCode, componentCount, nextTagValueOffset, output);
        } else {
            writeSmallValueTagInfoHeader(tagType, formatCode, componentCount, tagValue, fillLength, output);
        }

        return returnOffset;
    }

    protected Object getTransformedTagValue(int tagType) {

        return getDirectory().getObject(tagType);
    }

    private void writeCommonTagInfoHeader(int tagType, int formatCode, int componentCount, OutputStream output) throws IOException {

        dataFormat.write16bits(tagType, output);
        dataFormat.write16bits(formatCode, output);
        dataFormat.write32bits(componentCount, output);
    }

    protected void writeOffsetTagInfoHeader(int tagType, int formatCode, int componentCount, int nextTagValueOffset,
            OutputStream output) throws IOException {

        if (LOG.isDebugEnabled()) {
            debugStream("TagHeader: " + getDirectory().getTagName(tagType) + " for offset " + nextTagValueOffset, output);
        }

        writeCommonTagInfoHeader(tagType, formatCode, componentCount, output);
        dataFormat.write32bits(nextTagValueOffset, output);

    }

    protected void writeSmallValueTagInfoHeader(int tagType, int formatCode, int componentCount, Object tagValue, int fillLength,
            OutputStream output) throws IOException {

        if (LOG.isDebugEnabled()) {
            debugStream("TagHeader: " + getDirectory().getTagName(tagType) + "," + formatCode + "," + componentCount
                    + " as small value " + tagValue, output);
        }

        writeCommonTagInfoHeader(tagType, formatCode, componentCount, output);
        dataFormat.writeValue(tagValue, formatCode, output);
        // Fill to 12 bytes
        for (int i = 0; i < fillLength; i++) {
            output.write(0x00);
        }

    }

    protected int getTagDataLength(int tagType, Object tagValue, int formatCode) {

        int tagLength;
        if (tagValue instanceof Directory) {
            DirectoryWriter directoryWriter = getDirectoryWriter(tagType, (Directory) tagValue, 0);
            tagLength = directoryWriter.getDirectoryLength();
        } else {
            tagLength = dataFormat.getFormattedSize(tagValue, formatCode);
        }
        return tagLength;
    }

    /**
     * @param tagType
     * @param directory
     * @param startOffset
     * 
     * @return
     */
    protected DirectoryWriter getDirectoryWriter(int tagType, Directory directory, int startOffset) {

        LOG.warn("No writer for subdirectory tag " + tagType + ":" + directory);
        return new SkipTagDirectoryWriter();
    }

    public int getDirectoryLength() {

        // 2 tagCount
        // + 12 per tag
        // + 4 linked directory offset
        // + for each tag > 4 length of tag
        // + linked directory length

        int directoryLength = 2 + 12 * getWriteableTagCount() + 4;

        Iterator<Tag> iterator = getWriteableTagIterator();
        while (iterator.hasNext()) {
            Tag tag = iterator.next();
            int tagLength = getTagDataLength(tag.getTagType(), getDirectory().getObject(tag.getTagType()), tag.getFormatCode());
            if (tagLength > 4) {
                directoryLength += tagLength;
            }
        }

        directoryLength += getLinkedDirectoryWriter(0).getDirectoryLength();

        return directoryLength;

    }

    protected Iterator<Tag> getWriteableTagIterator() {

        return getDirectory().getTagIterator();
    }

    protected int getWriteableTagCount() {

        return getDirectory().getTagCount();
    }

    /**
     * @param startOffset
     * @return
     */
    protected DirectoryWriter getLinkedDirectoryWriter(int startOffset) {

        if (directory.getLinkedDirectory() != null) {
            LOG.warn("No writer for linked directory " + directory.getLinkedDirectory());
        }
        return new SkipTagDirectoryWriter();
    }

    public void setDataFormat(TiffTagFormat dataFormat) {

        this.dataFormat = dataFormat;

    }

    public TiffTagFormat getDataFormat() {

        return dataFormat;
    }

    protected Directory getDirectory() {

        return directory;
    }

    protected int getStartOffset() {

        return startOffset;
    }

    public void setStartOffset(int startOffset) {

        this.startOffset = startOffset;
    }

    protected void debugStream(String message, OutputStream output) {

        StringBuffer msg = new StringBuffer(message);
        if (output instanceof LoggingOutputStream) {
            msg.append(" at ");
            msg.append(((LoggingOutputStream) output).getStreamPosition());
        }
        LOG.debug(msg.toString());
    }

    public ExifTransform getExifTransform() {

        return transform;
    }

}